﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace ExpressionEngine
{
    /// <summary>
    /// Allows to evaluate mathematical expression
    /// </summary>
    public class ExpressionEveluator
    {
        // to check if expression part is number
        private static Regex _isNumberRegEx = new Regex(@"(\d+)|(\d+\.{0,1}\d+)");
        // to check if expression part is variable
        private static Regex _isVariableRegEx = new Regex(@"[a-zA-Z]+[a-zA-Z\d]*"); 
        // to split infix expression into logical parts
        private static Regex _expressionPartsSplitter = new Regex( 
@"(\d+\.{0,1}\d+)" // all numbers with decimal point
+ @"|"
+ @"(\d+)" // all integer numbers
+ @"|"
+ @"(^[\+\-]{1}((\d+\.{1}\d+)|(\d+)))" // all integer and floating point number at the beginning of the string with + or - specified
+ @"|"
+ @"((?<=[+\/\*\(\-])[\+\-]{1}((\d+\.{1}\d+)|(\d+)))" // all integer and floating point number that goes after '+','-','*','/','(' with + or - specified
+ @"|"
+ @"([a-zA-Z]+[a-zA-Z\d]*)" //all possible variable names
+ @"|"
+ @"([\(\\/\+\-\*)]{1})")  // rest parts of expression - '-', '+', '*', '/', '(', ')' 
;

        private List<string> _postfixExpression = new List<string>();
        private bool _postfixExpressionPrepared;
        
        /// <summary>
        /// Initializes a new instance of the ExpressionEngine.ExpressionEveluator class using the
        /// specified expression.
        /// </summary>
        /// <param name="expression">expression string</param>
        public ExpressionEveluator(string expression)
        {
            Expression = expression;
        }

        /// <summary>
        /// Expression string
        /// </summary>
        public string Expression { get; protected set; }

        /// <summary>
        /// Evaluates expression
        /// </summary>
        /// <param name="provider">value provider allows to get concrete values of variables specified in expression.
        /// Can be null if there are no variables in expression.
        /// </param>
        /// <returns>expression evaluation result</returns>
        public double Evaluate(IValueProvider provider)
        {
            if (!_postfixExpressionPrepared)
            {
                PreparePostfixExpression();
                _postfixExpressionPrepared = true;
            }

            Stack<double> stack = new Stack<double>();

            foreach (string item in _postfixExpression)
            {
                if (item.Length == 1 && IsOperator(item))
                {
                    double secondArg = stack.Pop();
                    double firstArg = stack.Pop();

                    stack.Push(ComputeOperand(item.First(), firstArg, secondArg));
                }
                else
                {
                    if (IsVariable(item))
                    {
                        if(provider == null)
                            throw new InvalidOperationException(string.Format("Variable {0} is not defined.", item));
                        stack.Push(provider.GetValue(item));
                    }
                    else
                        stack.Push(Double.Parse(item, System.Globalization.CultureInfo.InvariantCulture.NumberFormat));
                }
            }

            return stack.Pop();
        }

        private void PreparePostfixExpression()
        {
            List<string> infixExpressionParts = _expressionPartsSplitter
                .Matches(
                    Expression.Replace(" ", string.Empty))
                .Cast<Match>()
                .Select(m => m.Value).ToList();
            Stack<string> stack = new Stack<string>();
            List<string> postfix = new List<string>();

            foreach (string expressionPart in infixExpressionParts)
            {
                if (IsNumber(expressionPart))
                {
                    postfix.Add(expressionPart);
                }
                else if (IsVariable(expressionPart))
                {
                    postfix.Add(expressionPart);
                }
                else if (expressionPart == "(")
                    stack.Push(expressionPart);
                else if (IsOperator(expressionPart))
                {
                    while (stack.Count > 0 && stack.Peek() != "(")
                    {
                        if (Prioritise(stack.Peek()) >= Prioritise(expressionPart))
                            postfix.Add(stack.Pop());
                        else
                            break;
                    }
                    stack.Push(expressionPart);
                }
                else if (expressionPart == ")")
                {
                    while (stack.Count > 0 && stack.Peek() != "(")
                    {
                        postfix.Add(stack.Pop());
                    }
                    stack.Pop();
                }
            }

            while (stack.Count > 0)
                postfix.Add(stack.Pop());

            _postfixExpression = postfix;
        }

        private static bool IsNumber(string part)
        {
            return _isNumberRegEx.IsMatch(part);
        }

        private static bool IsVariable(string part)
        {
            return _isVariableRegEx.IsMatch(part);
        }
        
        private static bool IsOperator(string part)
        {
            return (part == "+" || part == "-" || part == "*" || part == "/");
        }

        private static int Prioritise(string @operator)
        {
            if (@operator == "-" || @operator == "+")
                return 10;
            if (@operator == "*" || @operator == "/")
                return 20;
            throw new NotSupportedException();
        }

        private static double ComputeOperand(char @operator, double a, double b)
        {
            switch (@operator)
            {
                case '-':
                    return a - b;
                case '+':
                    return a + b;
                case '*':
                    return a * b;
                case '/':
                    {
                        if (b == 0)
                            throw new DivideByZeroException();
                        return a / b;
                    }
                default:
                    throw new NotSupportedException(string.Format("Operator {0} is not supported.", @operator));
            }
        }
    }
}
